using System.Diagnostics;

namespace CCStatistics.Domain.Api
{
    using System;
    using System.Collections.Generic;
    using System.IO;
    using System.Xml;
    using System.Xml.XPath;

    using Interfaces;

    using ThoughtWorks.CruiseControl.Core;
    using ThoughtWorks.CruiseControl.Core.Publishers.Statistics;

    /// <summary>
    /// The first difference comes down to one line in the "Run" method where we want to use a different override of 
    /// StatisticsBuilder.ProcessBuildResults. Normal execution would write the IIntegrationResult and process its contents. 
    /// In this circumstance we already have the results of writing the xml file in as we are processing an already generated 
    /// log file, and it would (probably) be difficult to deconstruct it back into the IIntegrationResult.
    /// 
    /// The second difference is when writing out the result file. We do not want to use "Now" as the timestamp
    /// for the integration - instead it should use the integration end time.
    /// </summary>
    public class CCStatisticsPublisher : StatisticsPublisher, ICCStatisticsPublisher
    {
        // private const string _csvFileName = "statistics.csv";

        private bool _updateExistingStatistics = true;
        private bool _removeObsoleteStatistics = true;

        /// <summary>
        /// Initializes a new instance of the <see cref="CCStatisticsPublisher"/> class.
        /// </summary>
        /// <param name="configuredStatistics">The configured statistics.</param>
        public CCStatisticsPublisher(IEnumerable<Statistic> configuredStatistics)
        {
            ConfiguredStatistics = new List<Statistic>(configuredStatistics);

            var builder = new StatisticsBuilder();
            for (var i = 0; i < ConfiguredStatistics.Count; i++)
            {
                var statistic = ConfiguredStatistics[i];
                if (statistic.Include)
                    builder.Statistics.Add(statistic);
                else
                    builder.Statistics.Remove(statistic);
            }

            ActiveStatistics = new List<Statistic>(builder.Statistics);
        }

        /// <summary>
        /// Gets the specific configured statistics, as loaded from the ccnet.config by CruiseControl.  
        /// This may include deactivated statistics (ie with Include=false).
        /// </summary>
        public new List<Statistic> ConfiguredStatistics { get; private set; }

        /// <summary>
        /// Gets the active statistics.  This includes the default stats determined by <see cref="StatisticsBuilder"/>, and 
        /// any configured additons or exclusions to these contained in <see cref="ICCStatisticsPublisher.ConfiguredStatistics"/>.
        /// </summary>
        public List<Statistic> ActiveStatistics { get; private set; }


        #region ICCStatisticsPublisher Members

        /// <summary>
        /// Specifies whether to recalculate values and update existing statistics nodes in the Xml document, or only add the new statistics from ConfiguredStatistics
        /// </summary>
        public bool UpdateExistingStatistics
        {
            get { return _updateExistingStatistics; }
            set { _updateExistingStatistics = value; }
        }

        /// <summary>
        /// Specifies whether to remove any existing statistics nodes from the Xml document if they are no longer mentioned in the ConfiguredStatistics
        /// </summary>
        public bool RemoveObsoleteStatistics
        {
            get { return _removeObsoleteStatistics; }
            set { _removeObsoleteStatistics = value; }
        }

        /// <summary>
        /// Different override than the standard Run method as we want to pass the logFileXml directly
        /// into the builder.ProcessBuildResults method.
        /// </summary>
        public void Run(IIntegrationResult integrationResult, string logXmlString)
        {
            internalRun(logXmlString, null, integrationResult);
        }

        private void internalRun(string logXmlString, XPathNavigator logXml, IIntegrationResult integrationResult)
        {
            if (logXml == null)
            {
                logXml = new XPathDocument(new StringReader(logXmlString)).CreateNavigator();
            }
            
            UpdateXmlFile(logXml, ActiveStatistics, integrationResult);

            //TODO - renable the Update of the CSV version of the statistics, either merging the same changes, or perhaps more simply: just recreating it from the new Xml File
            //UpdateCsvFile(builder, integrationResult);
        }


        /// <summary>
        /// Proceses the log file.
        /// </summary>
        /// <param name="logFileName">Name of the log file.</param>
        /// <param name="statsProject">The _project.</param>
        /// <param name="nextLabel">The last label obtained, usually derived from a previous successful build of the project</param>
        public int ProcessLogFile(string logFileName, ICCStatsProject statsProject, int nextLabel)
        {
            if (String.IsNullOrEmpty(logFileName))
                throw new ArgumentNullException("logFileName");

            if (statsProject == null)
                throw new ArgumentNullException("statsProject");

            if (String.IsNullOrEmpty(statsProject.BuildLogDirectory))
                throw new InvalidOperationException(
                    "The BuildLogDirectory is not defined on the ICCStatsProject. Processing cannot continue");

            var LogFileLocation = Path.Combine(statsProject.BuildLogDirectory, logFileName);
            String logXmlString;

            IIntegrationResult result;
            XPathNavigator logxml;

            using (XmlReader xmlReader = new XmlTextReader(LogFileLocation))
            {
                logxml = new XPathDocument(xmlReader).CreateNavigator();
                logXmlString = logxml.OuterXml;

                result = IntegrationResultHelper.Result(LogFileLocation,
                                                        logxml,
                                                        statsProject.WorkingDirectory,
                                                        statsProject.ArtifactDirectory, 
                                                        nextLabel);

                xmlReader.Close();
            }

            internalRun(logXmlString, logxml, result);

            //guess the next Build label, based on CruiseControl's normal behaviour of incrementing after successful builds
            int thisBuildLabel;
            return result.Succeeded && Int32.TryParse(result.Label, out thisBuildLabel) ? thisBuildLabel + 1 : nextLabel;
        }

        #endregion

        private void UpdateXmlFile(XPathNavigator logxml, IEnumerable<Statistic> stats, IIntegrationResult integrationResult)
        {
            var doc = new XmlDocument();
            var lastFile = XmlStatisticsFile(integrationResult);

            XmlElement root;
            if (File.Exists(lastFile))
            {
                doc.Load(lastFile);
                root = (XmlElement)doc.FirstChild;
            }
            else
            {
                root = doc.CreateElement("statistics");
                doc.AppendChild(root);
            }

            //var xmlnsManager = new XmlNamespaceManager(doc.NameTable);
            //xmlnsManager.AddNamespace("op", "http://www.w3.org/2003/05/xpath-operators");

            var xNav = root.CreateNavigator();

            var buildStartTime = integrationResult.StartTime;
            var startTimeInCCnetFormat = buildStartTime.ToString("s").Replace("T", " ");
            var xPathForBuildNode = string.Format("integration/statistic[@name='StartTime' and text()='{0}']/..", startTimeInCCnetFormat);
            var xmlForThisBuild = (XmlElement)root.SelectSingleNode(xPathForBuildNode);
            if (xmlForThisBuild == null)
            {
                xmlForThisBuild = root.OwnerDocument.CreateElement("integration");
                xmlForThisBuild.SetAttribute("build-label", integrationResult.Label);

                var status = integrationResult.Status;
                xmlForThisBuild.SetAttribute("status", status.ToString());
                xmlForThisBuild.SetAttribute("day", buildStartTime.Day.ToString());
                xmlForThisBuild.SetAttribute("month", buildStartTime.ToString("MMM"));
                xmlForThisBuild.SetAttribute("year", buildStartTime.Year.ToString());

                RenderToXml(xmlForThisBuild, logxml, stats.GetEnumerator());

                //nb: because this internally uses XPathNavigator's Insert/Append methods, which take a copy of the current Xml of the node, 
                //we need to populate the xmlForThisBuild node in the call to RenderToXml above before we call insert...
                insertNodeByBuildStartTime(buildStartTime, xmlForThisBuild, xNav);
            } 
            else
            {
                RenderToXml(xmlForThisBuild, logxml, stats.GetEnumerator());
            }

            Directory.CreateDirectory(integrationResult.ArtifactDirectory);
            doc.Save(XmlStatisticsFile(integrationResult));
        }

        private static void insertNodeByBuildStartTime(DateTime buildStartTime, IXPathNavigable xmlForThisBuild, XPathNavigator xNav)
        {
            var selectKey = xNav.Compile("integration/statistic[@name='StartTime']");
            selectKey.AddSort(".", XmlSortOrder.Ascending, XmlCaseOrder.None, "", XmlDataType.Text);
            var sortedNodes = xNav.Select(selectKey);

            var foundPlaceToInsert = false;
            foreach (XPathNavigator curNode in sortedNodes)
            {
                var curNodeTime = Convert.ToDateTime(curNode.TypedValue);
                if (buildStartTime < curNodeTime)
                {
                    curNode.MoveToParent();
                    curNode.InsertBefore(xmlForThisBuild.CreateNavigator());
                    foundPlaceToInsert = true;
                    break;
                }
            }
            if (!foundPlaceToInsert)
                xNav.AppendChild(xmlForThisBuild.CreateNavigator());
        }

        /// <summary>
        /// Publish the statistics to the output XmlElement.
        /// </summary>
        /// <param name="integrationElement">The element corresponding to this build to which the statistics should be added</param>
        /// <param name="logxml">An XPathNavigator over the XML build results</param>
        /// <param name="stats">The list of stats to evaluate and output</param>
        /// <returns></returns>
        private void RenderToXml(XmlElement integrationElement, XPathNavigator logxml, IEnumerator<Statistic> stats)
        {
            if (integrationElement == null)
                throw new ArgumentNullException("integrationElement");

            var unmatchedExistingStatsElements = new List<XmlElement>();

            //if we are to remove any old existing elements that do not correspond to a current ConfiguredStatistics entry, then we need to get a list of all existing nodes
            if (_removeObsoleteStatistics)
            {
                var existingNodes = integrationElement.SelectNodes("statistic");
                if (existingNodes != null)
                    foreach (XmlElement existingNode in existingNodes)
                        unmatchedExistingStatsElements.Add(existingNode);
            }
            while (stats.MoveNext())
            {
                var currentStat = stats.Current;
                bool isMissingStat;
                var xmlStatsElement = (XmlElement)integrationElement.SelectSingleNode(string.Format("statistic[@name='{0}']", currentStat.Name));
                if (xmlStatsElement == null)
                {
                    isMissingStat = true;
                    xmlStatsElement = integrationElement.OwnerDocument.CreateElement("statistic");
                    xmlStatsElement.SetAttribute("name", currentStat.Name);
                }
                else
                {
                    isMissingStat = false;
                    if (_removeObsoleteStatistics)
                        unmatchedExistingStatsElements.Remove(xmlStatsElement);
                }

                if (isMissingStat || _updateExistingStatistics)
                {
                    StatisticResult statisticResult;
                    try
                    {
                        statisticResult = currentStat.Apply(logxml);
                    }
                    catch (Exception ex)
                    {
                        statisticResult = new StatisticResult(currentStat.Name, ex.Message);
                    }

                    var statResultAsString = Convert.ToString(statisticResult.Value);

                    //Debug.Assert(xmlStatsElement.InnerText == statResultAsString);

                    xmlStatsElement.InnerText = statResultAsString;
                    integrationElement.AppendChild(xmlStatsElement);
                }
            }

            if (_removeObsoleteStatistics)
                foreach (var staleStatsNode in unmatchedExistingStatsElements)
                    integrationElement.RemoveChild(staleStatsNode);
        }

        private static string XmlStatisticsFile(IIntegrationResult integrationResult)
        {
            return Path.Combine(integrationResult.ArtifactDirectory, GetMasterStatsFile(integrationResult));
        }

        private static string GetMasterStatsFile(IIntegrationResult integrationResult)
        {
            return Path.Combine(integrationResult.ArtifactDirectory, @"report.xml");
        }

        //private static void UpdateCsvFile(StatisticsBuilder builder, IIntegrationResult previousState)
        //{
        //    var csvFile = CsvStatisticsFile(previousState);
        //    builder.AppendCsv(csvFile);
        //}

        //private static string CsvStatisticsFile(IIntegrationResult integrationState)
        //{
        //    return Path.Combine(integrationState.ArtifactDirectory, _csvFileName);
        //}
    }
}